import os
import csv
import logging
from typing import List, Optional
from bs4 import BeautifulSoup
import requests
from datetime import datetime, timedelta
from crawlers.base import BaseCrawler
from models.lottery import LotteryDraw, LotteryManager
from models.config import LOTTERY_TYPES, CRAWLER_CONFIG

# SSQCrawler
class SSQCrawler(BaseCrawler):
    """双色球爬虫"""
    def __init__(self):
        super().__init__('ssq')  # 传递彩票类型
        self.config = LOTTERY_TYPES[self.lottery_type]
        self.headers = {
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
            "Referer": "https://m.17500.cn/kj-m/list-ssq.html",
            "Origin": "https://m.17500.cn",
            "Host": "m.17500.cn",
            "Accept": "*/*",
            "Accept-Encoding": "gzip, deflate, br, zstd",
            "Accept-Language": "zh-CN,zh;q=0.9",
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "Content-Type": "application/x-www-form-urlencoded",
            "Pragma": "no-cache",
            "Sec-Ch-Ua": '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
            "Sec-Ch-Ua-Mobile": "?0",
            "Sec-Ch-Ua-Platform": '"macOS"',
            "Sec-Fetch-Dest": "empty",
            "Sec-Fetch-Mode": "cors",
            "Sec-Fetch-Site": "same-origin"
        }
        self.data_file = self.config['data_file']
        self.ensure_data_dir()

    def ensure_data_dir(self):
        data_dir = os.path.dirname(self.data_file)
        if data_dir and not os.path.exists(data_dir):
            os.makedirs(data_dir)

    def get_existing_issues(self):
        if not os.path.exists(self.data_file):
            return set()
        with open(self.data_file, 'r') as f:
            reader = csv.DictReader(f)
            return {row['draw_number'] for row in reader}

    def fetch_latest_draw(self):
        try:
            url = self.config['url']
            self.logger.info(f"开始请求URL: {url}")
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            draw_divs = soup.find_all('div', class_='flex p10')
            self.logger.info(f"找到 {len(draw_divs)} 个开奖数据项")
            if not draw_divs:
                self.logger.error("未找到任何开奖数据项")
                self.logger.debug(f"页面内容: {response.text[:1000]}")
                return None
            for div in draw_divs:
                try:
                    self.logger.debug(f"当前处理的div内容: {div.prettify()}")
                    issue_tag = div.find('b', attrs={'data-name': 'issue'})
                    if not issue_tag:
                        continue
                    issue = issue_tag.get('data-v', '').replace('期', '')
                    if not issue:
                        continue
                    self.logger.info(f"处理期号: {issue}")
                    time_tag = div.find('i', class_='mlauto')
                    if not time_tag:
                        self.logger.warning(f"期号 {issue} 未找到时间标签")
                        self.logger.debug(f"当前div内容: {div.prettify()}")
                        continue
                    time_text = time_tag.text.strip()
                    if not time_text:
                        self.logger.error(f"期号 {issue} 的时间文本为空")
                        continue
                    self.logger.info(f"时间文本: {time_text}")
                    try:
                        if '今天' in time_text:
                            draw_date = datetime.now().strftime('%Y-%m-%d')
                        elif '昨天' in time_text:
                            draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                        else:
                            draw_date = time_text.split()[0]
                            datetime.strptime(draw_date, '%Y-%m-%d')
                    except (ValueError, IndexError) as e:
                        self.logger.error(f"日期格式不正确: {time_text}, 错误: {str(e)}")
                        continue
                    self.logger.info(f"处理后的日期: {draw_date}")
                    try:
                        copy_link = div.find('a', attrs={'data-copy': 'true'})
                        if not copy_link:
                            self.logger.error(f"期号 {issue} 未找到带有 data-copy 属性的链接")
                            self.logger.debug(f"尝试其他方式获取号码...")
                            ball_container = div.find('p', class_='ball flex mt2 flex-wrap')
                            if ball_container:
                                red_balls = ball_container.select('b.rb')
                                blue_balls = ball_container.select('b.bb')
                                if len(red_balls) == 6 and len(blue_balls) == 1:
                                    red_numbers = [ball.text.strip() for ball in red_balls]
                                    blue_number = blue_balls[0].text.strip()
                                    lottery_data = {
                                        'draw_number': issue,
                                        'draw_date': draw_date,
                                        'red_numbers': ','.join(red_numbers),
                                        'blue_number': blue_number
                                    }
                                    self.logger.info(f"使用备选方式获取号码成功: {lottery_data}")
                                    lottery_list.append(LotteryDraw(**lottery_data))
                                    continue
                            self.logger.error("所有获取号码的方式都失败")
                            continue
                        numbers_text = copy_link.get('data-v', '')
                        if not numbers_text:
                            self.logger.error(f"期号 {issue} 的开奖号码为空")
                            continue
                        self.logger.info(f"原始号码文本: {numbers_text}")
                        if '+' in numbers_text:
                            red_text, blue_text = numbers_text.split('+')
                            red_numbers = [num.strip() for num in red_text.split()]
                            blue_number = blue_text.strip()
                            if len(red_numbers) == 6:
                                self.logger.info(f"处理后的号码: 红球={red_numbers}, 蓝球={blue_number}")
                                lottery_data = {
                                    'draw_number': issue,
                                    'draw_date': draw_date,
                                    'red_numbers': ','.join(red_numbers),
                                    'blue_number': blue_number
                                }
                                self.logger.info(f"构建的数据: {lottery_data}")
                                lottery_list.append(LotteryDraw(**lottery_data))
                            else:
                                self.logger.error(f"期号 {issue} 的号码格式不正确: 红球={len(red_numbers)}个, 蓝球={blue_number}")
                        else:
                            self.logger.error(f"期号 {issue} 的号码格式不正确: {numbers_text}")
                    except Exception as e:
                        self.logger.error(f"处理号码时出错: {str(e)}")
                        self.logger.exception("详细错误信息")
                        continue
                except Exception as e:
                    self.logger.error(f"解析开奖数据失败: {str(e)}")
                    self.logger.exception("详细错误信息")
                    continue
            if lottery_list:
                self.logger.info(f"成功获取 {len(lottery_list)} 条数据")
                return lottery_list[0]
            self.logger.warning("未找到有效数据")
            return None
        except Exception as e:
            self.logger.error(f"获取最新一期数据失败: {str(e)}")
            return None

    def fetch_history_data(self):
        try:
            url = self.config['url']
            self.logger.info(f"开始请求URL: {url}")
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            draw_divs = soup.find_all('div', class_='flex p10')
            self.logger.info(f"找到 {len(draw_divs)} 个开奖数据项")
            for div in draw_divs:
                try:
                    issue_tag = div.find('b', attrs={'data-name': 'issue'})
                    if not issue_tag:
                        continue
                    issue = issue_tag.get('data-v', '').replace('期', '')
                    if not issue:
                        continue
                    self.logger.info(f"处理期号: {issue}")
                    time_tag = div.find('i', class_='mlauto')
                    if not time_tag:
                        self.logger.warning(f"期号 {issue} 未找到时间标签")
                        continue
                    time_text = time_tag.text.strip()
                    self.logger.info(f"时间文本: {time_text}")
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        try:
                            draw_date = time_text.split()[0]
                        except (ValueError, IndexError):
                            self.logger.error(f"日期格式不正确: {time_text}")
                            continue
                    try:
                        copy_link = div.find('a', attrs={'data-copy': 'true'})
                        if not copy_link:
                            ball_container = div.find('p', class_='ball flex mt2 flex-wrap')
                            if ball_container:
                                red_balls = ball_container.select('b.rb')
                                blue_balls = ball_container.select('b.bb')
                                if len(red_balls) == 6 and len(blue_balls) == 1:
                                    red_numbers = [ball.text.strip() for ball in red_balls]
                                    blue_number = blue_balls[0].text.strip()
                                    lottery_data = {
                                        'draw_number': issue,
                                        'draw_date': draw_date,
                                        'red_numbers': ','.join(red_numbers),
                                        'blue_number': blue_number
                                    }
                                    lottery_list.append(LotteryDraw(**lottery_data))
                                    continue
                            continue
                        numbers_text = copy_link.get('data-v', '')
                        if not numbers_text:
                            continue
                        if '+' in numbers_text:
                            red_text, blue_text = numbers_text.split('+')
                            red_numbers = [num.strip() for num in red_text.split()]
                            blue_number = blue_text.strip()
                            if len(red_numbers) == 6:
                                lottery_data = {
                                    'draw_number': issue,
                                    'draw_date': draw_date,
                                    'red_numbers': ','.join(red_numbers),
                                    'blue_number': blue_number
                                }
                                lottery_list.append(LotteryDraw(**lottery_data))
                    except Exception as e:
                        continue
                except Exception as e:
                    continue
            return lottery_list
        except Exception as e:
            self.logger.error(f"获取历史数据失败: {str(e)}")
            return []

    def save_draws(self, draws):
        if not draws:
            return
        fieldnames = ['draw_number', 'draw_date', 'red_numbers', 'blue_number']
        file_exists = os.path.exists(self.data_file)
        has_header = False
        if file_exists:
            with open(self.data_file, 'r', newline='') as f:
                reader = csv.reader(f)
                first_line = next(reader, None)
                has_header = first_line and first_line == fieldnames
        if not file_exists or not has_header:
            with open(self.data_file, 'w', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()
                writer.writerows([draw.__dict__ for draw in draws])
            self.logger.info(f"创建新文件并保存 {len(draws)} 条数据")
            return
        try:
            existing_issues = self.get_existing_issues()
            new_data = [draw for draw in draws if draw.draw_number not in existing_issues]
            if not new_data:
                return
            with open(self.data_file, 'a', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writerows([draw.__dict__ for draw in new_data])
            self.logger.info(f"成功保存 {len(new_data)} 条新数据")
        except Exception as e:
            self.logger.error(f"保存数据失败: {str(e)}")
            with open(self.data_file, 'w', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()
                writer.writerows([draw.__dict__ for draw in draws])
            self.logger.info(f"重新创建文件并保存 {len(draws)} 条数据")

# DLTCrawler
class DLTCrawler(BaseCrawler):
    """大乐透爬虫"""
    def __init__(self):
        super().__init__('dlt')
        self.logger = logging.getLogger('dlt_crawler')
        self.manager = LotteryManager('dlt')
        self.config = LOTTERY_TYPES[self.lottery_type]
        self.headers = CRAWLER_CONFIG['headers']
        self.timeout = CRAWLER_CONFIG['timeout']
        self.max_retries = CRAWLER_CONFIG['max_retries']
        self.retry_delay = CRAWLER_CONFIG['retry_delay']
        self.data_file = self.config['data_file']
        self.ensure_data_dir()

    def ensure_data_dir(self):
        data_dir = os.path.dirname(self.data_file)
        if data_dir and not os.path.exists(data_dir):
            os.makedirs(data_dir)

    def get_existing_issues(self):
        if not os.path.exists(self.data_file):
            return set()
        with open(self.data_file, 'r') as f:
            reader = csv.DictReader(f)
            return {row['draw_number'] for row in reader}

    def fetch_latest_draw(self) -> Optional[LotteryDraw]:
        try:
            url = self.config['url']
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            items = soup.find_all('p', class_='flex flex-align')
            for item in items:
                try:
                    issue = item.find('b', class_='mr10 f16').text.strip()
                    if '期' not in issue:
                        continue
                    issue = issue.replace('期', '')
                    time_text = item.find('i', class_='mlauto f12').text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        draw_date = time_text.split()[0]
                    ball_container = item.find_next_sibling('p', class_='ball flex mt2 flex-wrap')
                    if not ball_container:
                        continue
                    red_balls = ball_container.select('b.rb')
                    blue_balls = ball_container.select('b.bb')
                    if len(red_balls) == 5 and len(blue_balls) == 2:
                        red_numbers = [num.text.strip() for num in red_balls]
                        blue_numbers = [num.text.strip() for num in blue_balls]
                        lottery_data = {
                            'draw_number': issue,
                            'draw_date': draw_date,
                            'red_numbers': ','.join(red_numbers),
                            'blue_number': ','.join(blue_numbers)
                        }
                        lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    continue
            return lottery_list[0] if lottery_list else None
        except Exception as e:
            self.logger.error(f"获取最新一期数据失败: {str(e)}")
            return None

    def fetch_history_data(self) -> List[LotteryDraw]:
        try:
            url = self.config['url']
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            items = soup.find_all('p', class_='flex flex-align')
            for item in items:
                try:
                    issue = item.find('b', class_='mr10 f16').text.strip()
                    if '期' not in issue:
                        continue
                    issue = issue.replace('期', '')
                    time_text = item.find('i', class_='mlauto f12').text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        draw_date = time_text.split()[0]
                    ball_container = item.find_next_sibling('p', class_='ball flex mt2 flex-wrap')
                    if not ball_container:
                        self.logger.error("未找到号码容器")
                        continue
                    red_balls = ball_container.select('b.rb')
                    blue_balls = ball_container.select('b.bb')
                    if len(red_balls) == 5 and len(blue_balls) == 2:
                        red_numbers = [num.text.strip() for num in red_balls]
                        blue_numbers = [num.text.strip() for num in blue_balls]
                        lottery_data = {
                            'draw_number': issue,
                            'draw_date': draw_date,
                            'red_numbers': ','.join(red_numbers),
                            'blue_number': ','.join(blue_numbers)
                        }
                        lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    self.logger.error(f"解析开奖数据失败: {str(e)}")
                    continue
            return lottery_list
        except Exception as e:
            self.logger.error(f"获取历史数据失败: {str(e)}")
            return []

    def save_draws(self, draws):
        if not draws:
            return
        fieldnames = ['draw_number', 'draw_date', 'red_numbers', 'blue_number']
        file_exists = os.path.exists(self.data_file)
        has_header = False
        if file_exists:
            with open(self.data_file, 'r', newline='') as f:
                reader = csv.reader(f)
                first_line = next(reader, None)
                has_header = first_line and first_line == fieldnames
        if not file_exists or not has_header:
            with open(self.data_file, 'w', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()
                writer.writerows([draw.__dict__ for draw in draws])
            self.logger.info(f"创建新文件并保存 {len(draws)} 条数据")
            return
        try:
            existing_issues = self.get_existing_issues()
            new_data = [draw for draw in draws if draw.draw_number not in existing_issues]
            if not new_data:
                return
            with open(self.data_file, 'a', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writerows([draw.__dict__ for draw in new_data])
            self.logger.info(f"成功保存 {len(new_data)} 条新数据")
        except Exception as e:
            self.logger.error(f"保存数据失败: {str(e)}")
            with open(self.data_file, 'w', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()
                writer.writerows([draw.__dict__ for draw in draws])
            self.logger.info(f"重新创建文件并保存 {len(draws)} 条数据")

# FC3DCrawler
class FC3DCrawler(BaseCrawler):
    """福彩3D爬虫"""
    def __init__(self):
        super().__init__('3d')
        self.logger = logging.getLogger('fc3d_crawler')
        self.config = LOTTERY_TYPES[self.lottery_type]
        self.headers = CRAWLER_CONFIG['headers']
        self.timeout = CRAWLER_CONFIG['timeout']
        self.max_retries = CRAWLER_CONFIG['max_retries']
        self.retry_delay = CRAWLER_CONFIG['retry_delay']
        self.data_file = self.config['data_file']
        self.ensure_data_dir()

    def ensure_data_dir(self):
        data_dir = os.path.dirname(self.data_file)
        if data_dir and not os.path.exists(data_dir):
            os.makedirs(data_dir)

    def get_existing_issues(self):
        if not os.path.exists(self.data_file):
            return set()
        with open(self.data_file, 'r') as f:
            reader = csv.DictReader(f)
            return {row['draw_number'] for row in reader}

    def fetch_latest_draw(self) -> Optional[LotteryDraw]:
        try:
            url = self.config['url']
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            items = soup.find_all('p', class_='flex flex-align')
            for item in items:
                try:
                    issue = item.find('b', class_='mr10 f16').text.strip()
                    if '期' not in issue:
                        continue
                    issue = issue.replace('期', '')
                    time_text = item.find('i', class_='mlauto f12').text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        draw_date = time_text.split()[0]
                    ball_container = item.find_next_sibling('p', class_='ball flex mt2 flex-wrap')
                    if not ball_container:
                        continue
                    balls = ball_container.select('b.rb')
                    if len(balls) == 3:
                        numbers = [num.text.strip() for num in balls]
                        lottery_data = {
                            'draw_number': issue,
                            'draw_date': draw_date,
                            'red_numbers': ','.join(numbers),
                            'blue_number': ''
                        }
                        lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    continue
            return lottery_list[0] if lottery_list else None
        except Exception as e:
            return None

    def fetch_history_data(self) -> List[LotteryDraw]:
        try:
            url = self.config['url']
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            items = soup.find_all('p', class_='flex flex-align')
            for item in items:
                try:
                    issue = item.find('b', class_='mr10 f16').text.strip()
                    if '期' not in issue:
                        continue
                    issue = issue.replace('期', '')
                    time_text = item.find('i', class_='mlauto f12').text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        draw_date = time_text.split()[0]
                    ball_container = item.find_next_sibling('p', class_='ball flex mt2 flex-wrap')
                    if not ball_container:
                        continue
                    balls = ball_container.select('b.rb')
                    if len(balls) == 3:
                        numbers = [num.text.strip() for num in balls]
                        lottery_data = {
                            'draw_number': issue,
                            'draw_date': draw_date,
                            'red_numbers': ','.join(numbers),
                            'blue_number': ''
                        }
                        lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    continue
            return lottery_list
        except Exception as e:
            return []

    def save_draws(self, draws):
        if not draws:
            return
        fieldnames = ['draw_number', 'draw_date', 'red_numbers', 'blue_number']
        file_exists = os.path.exists(self.data_file)
        has_header = False
        if file_exists:
            with open(self.data_file, 'r', newline='') as f:
                reader = csv.reader(f)
                first_line = next(reader, None)
                has_header = first_line and first_line == fieldnames
        if not file_exists or not has_header:
            with open(self.data_file, 'w', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()
                writer.writerows([draw.__dict__ for draw in draws])
            return
        try:
            existing_issues = self.get_existing_issues()
            new_data = [draw for draw in draws if draw.draw_number not in existing_issues]
            if not new_data:
                return
            with open(self.data_file, 'a', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writerows([draw.__dict__ for draw in new_data])
        except Exception as e:
            self.logger.error(f"保存数据失败: {str(e)}")
            with open(self.data_file, 'w', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()
                writer.writerows([draw.__dict__ for draw in draws])

# PL3Crawler
class PL3Crawler(BaseCrawler):
    """排列3爬虫"""
    def __init__(self):
        super().__init__('pl3')
        self.logger = logging.getLogger('pl3_crawler')
        self.config = LOTTERY_TYPES[self.lottery_type]
        self.headers = CRAWLER_CONFIG['headers']
        self.timeout = CRAWLER_CONFIG['timeout']
        self.max_retries = CRAWLER_CONFIG['max_retries']
        self.retry_delay = CRAWLER_CONFIG['retry_delay']
        self.data_file = self.config['data_file']
        self.ensure_data_dir()

    def ensure_data_dir(self):
        data_dir = os.path.dirname(self.data_file)
        if data_dir and not os.path.exists(data_dir):
            os.makedirs(data_dir)

    def get_existing_issues(self):
        if not os.path.exists(self.data_file):
            return set()
        with open(self.data_file, 'r') as f:
            reader = csv.DictReader(f)
            return {row['draw_number'] for row in reader}

    def fetch_latest_draw(self) -> Optional[LotteryDraw]:
        try:
            url = self.config['url']
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            items = soup.find_all('p', class_='flex flex-align')
            for item in items:
                try:
                    issue = item.find('b', class_='mr10 f16').text.strip()
                    if '期' not in issue:
                        continue
                    issue = issue.replace('期', '')
                    time_text = item.find('i', class_='mlauto f12').text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        draw_date = time_text.split()[0]
                    ball_container = item.find_next_sibling('p', class_='ball flex mt2 flex-wrap')
                    if not ball_container:
                        continue
                    balls = ball_container.select('b.rb')
                    if len(balls) == 3:
                        numbers = [num.text.strip() for num in balls]
                        lottery_data = {
                            'draw_number': issue,
                            'draw_date': draw_date,
                            'red_numbers': ','.join(numbers),
                            'blue_number': ''
                        }
                        lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    continue
            return lottery_list[0] if lottery_list else None
        except Exception as e:
            self.logger.error(f"获取最新一期数据失败: {str(e)}")
            return None

    def fetch_history_data(self) -> List[LotteryDraw]:
        try:
            url = self.config['url']
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            items = soup.find_all('p', class_='flex flex-align')
            for item in items:
                try:
                    issue = item.find('b', class_='mr10 f16').text.strip()
                    if '期' not in issue:
                        continue
                    issue = issue.replace('期', '')
                    time_text = item.find('i', class_='mlauto f12').text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        draw_date = time_text.split()[0]
                    ball_container = item.find_next_sibling('p', class_='ball flex mt2 flex-wrap')
                    if not ball_container:
                        continue
                    balls = ball_container.select('b.rb')
                    if len(balls) == 3:
                        numbers = [num.text.strip() for num in balls]
                        lottery_data = {
                            'draw_number': issue,
                            'draw_date': draw_date,
                            'red_numbers': ','.join(numbers),
                            'blue_number': ''
                        }
                        lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    continue
            return lottery_list
        except Exception as e:
            self.logger.error(f"获取历史数据失败: {str(e)}")
            return []

    def save_draws(self, draws):
        if not draws:
            return
        fieldnames = ['draw_number', 'draw_date', 'red_numbers', 'blue_number']
        file_exists = os.path.exists(self.data_file)
        has_header = False
        if file_exists:
            with open(self.data_file, 'r', newline='') as f:
                reader = csv.reader(f)
                first_line = next(reader, None)
                has_header = first_line and first_line == fieldnames
        if not file_exists or not has_header:
            with open(self.data_file, 'w', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()
                writer.writerows([draw.__dict__ for draw in draws])
            self.logger.info(f"创建新文件并保存 {len(draws)} 条数据")
            return
        try:
            existing_issues = self.get_existing_issues()
            new_data = [draw for draw in draws if draw.draw_number not in existing_issues]
            if not new_data:
                return
            with open(self.data_file, 'a', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writerows([draw.__dict__ for draw in new_data])
            self.logger.info(f"成功保存 {len(new_data)} 条新数据")
        except Exception as e:
            self.logger.error(f"保存数据失败: {str(e)}")
            with open(self.data_file, 'w', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()
                writer.writerows([draw.__dict__ for draw in draws])
            self.logger.info(f"重新创建文件并保存 {len(draws)} 条数据")

# PL5Crawler
class PL5Crawler(BaseCrawler):
    """排列5爬虫"""
    def __init__(self):
        super().__init__('pl5')
        self.logger = logging.getLogger('pl5_crawler')
        self.manager = LotteryManager('pl5')
        self.config = LOTTERY_TYPES[self.lottery_type]
        self.headers = CRAWLER_CONFIG['headers']
        self.timeout = CRAWLER_CONFIG['timeout']
        self.max_retries = CRAWLER_CONFIG['max_retries']
        self.retry_delay = CRAWLER_CONFIG['retry_delay']
        self.data_file = self.config['data_file']
        self.ensure_data_dir()

    def ensure_data_dir(self):
        data_dir = os.path.dirname(self.data_file)
        if data_dir and not os.path.exists(data_dir):
            os.makedirs(data_dir)

    def get_existing_issues(self):
        if not os.path.exists(self.data_file):
            return set()
        with open(self.data_file, 'r') as f:
            reader = csv.DictReader(f)
            return {row['draw_number'] for row in reader}

    def fetch_latest_draw(self) -> Optional[LotteryDraw]:
        try:
            url = self.config['url']
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            items = soup.find_all('p', class_='flex flex-align')
            for item in items:
                try:
                    issue = item.find('b', class_='mr10 f16').text.strip()
                    if '期' not in issue:
                        continue
                    issue = issue.replace('期', '')
                    time_text = item.find('i', class_='mlauto f12').text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        draw_date = time_text.split()[0]
                    ball_container = item.find_next_sibling('p', class_='ball flex mt2 flex-wrap')
                    if not ball_container:
                        continue
                    balls = ball_container.select('b.rb')
                    if len(balls) == 5:
                        numbers = [num.text.strip() for num in balls]
                        lottery_data = {
                            'draw_number': issue,
                            'draw_date': draw_date,
                            'red_numbers': ','.join(numbers),
                            'blue_number': ''
                        }
                        lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    continue
            return lottery_list[0] if lottery_list else None
        except Exception as e:
            return None

    def fetch_history_data(self) -> List[LotteryDraw]:
        try:
            url = self.config['url']
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            items = soup.find_all('p', class_='flex flex-align')
            for item in items:
                try:
                    issue = item.find('b', class_='mr10 f16').text.strip()
                    if '期' not in issue:
                        continue
                    issue = issue.replace('期', '')
                    time_text = item.find('i', class_='mlauto f12').text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        draw_date = time_text.split()[0]
                    ball_container = item.find_next_sibling('p', class_='ball flex mt2 flex-wrap')
                    if not ball_container:
                        continue
                    balls = ball_container.select('b.rb')
                    if len(balls) == 5:
                        numbers = [num.text.strip() for num in balls]
                        lottery_data = {
                            'draw_number': issue,
                            'draw_date': draw_date,
                            'red_numbers': ','.join(numbers),
                            'blue_number': ''
                        }
                        lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    continue
            return lottery_list
        except Exception as e:
            return []

    def save_draws(self, draws):
        if not draws:
            return
        fieldnames = ['draw_number', 'draw_date', 'red_numbers', 'blue_number']
        file_exists = os.path.exists(self.data_file)
        has_header = False
        if file_exists:
            with open(self.data_file, 'r', newline='') as f:
                reader = csv.reader(f)
                first_line = next(reader, None)
                has_header = first_line and first_line == fieldnames
        if not file_exists or not has_header:
            with open(self.data_file, 'w', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()
                writer.writerows([draw.__dict__ for draw in draws])
            return
        try:
            existing_issues = self.get_existing_issues()
            new_data = [draw for draw in draws if draw.draw_number not in existing_issues]
            if not new_data:
                return
            with open(self.data_file, 'a', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writerows([draw.__dict__ for draw in new_data])
            self.logger.info(f"成功保存 {len(new_data)} 条新数据")
        except Exception as e:
            self.logger.error(f"保存数据失败: {str(e)}")
            with open(self.data_file, 'w', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()
                writer.writerows([draw.__dict__ for draw in draws])
            self.logger.info(f"重新创建文件并保存 {len(draws)} 条数据")

# QXCCrawler
class QXCCrawler(BaseCrawler):
    """七星彩爬虫"""
    def __init__(self):
        super().__init__('7xc')
        self.logger = logging.getLogger('qxc_crawler')
        self.config = LOTTERY_TYPES[self.lottery_type]
        self.headers = CRAWLER_CONFIG['headers']
        self.timeout = CRAWLER_CONFIG['timeout']
        self.data_file = self.config['data_file']
        self.ensure_data_dir()

    def ensure_data_dir(self):
        data_dir = os.path.dirname(self.data_file)
        if data_dir and not os.path.exists(data_dir):
            os.makedirs(data_dir)

    def get_existing_issues(self):
        if not os.path.exists(self.data_file):
            return set()
        with open(self.data_file, 'r') as f:
            reader = csv.DictReader(f)
            return {row['draw_number'] for row in reader}

    def fetch_latest_draw(self):
        try:
            url = self.config['url']
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            draw_divs = soup.find_all('div', class_='flex p10')
            for div in draw_divs:
                try:
                    issue_tag = div.find('b', attrs={'data-name': 'issue'})
                    issue = issue_tag.get('data-v', '').replace('期', '')
                    time_tag = div.find('i', class_='mlauto')
                    if not time_tag:
                        continue
                    time_text = time_tag.text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        try:
                            draw_date = time_text.split()[0]
                        except (ValueError, IndexError):
                            continue
                    copy_link = div.find('a', attrs={'data-copy': 'true'})
                    if not copy_link:
                        continue
                    numbers_text = copy_link.get('data-v', '')
                    numbers = [num.strip() for num in numbers_text.split()]
                    if len(numbers) == 7:
                        red_numbers = numbers[:6]
                        blue_number = numbers[6]
                        lottery_data = {
                            'draw_number': issue,
                            'draw_date': draw_date,
                            'red_numbers': ','.join(red_numbers),
                            'blue_number': blue_number
                        }
                        lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    continue
            if lottery_list:
                return lottery_list[0]
            return None
        except Exception as e:
            return None

    def fetch_history_data(self) -> List[LotteryDraw]:
        try:
            response = requests.get(self.config['url'], headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            draw_divs = soup.find_all('div', class_='flex p10')
            for div in draw_divs:
                try:
                    issue_tag = div.find('b', attrs={'data-name': 'issue'})
                    if not issue_tag:
                        continue
                    issue = issue_tag.get('data-v', '').replace('期', '')
                    time_tag = div.find('i', class_='mlauto')
                    if not time_tag:
                        continue
                    time_text = time_tag.text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        try:
                            draw_date = time_text.split()[0]
                        except (ValueError, IndexError):
                            continue
                    copy_link = div.find('a', attrs={'data-copy': 'true'})
                    if not copy_link:
                        continue
                    numbers_text = copy_link.get('data-v', '')
                    numbers = [num.strip() for num in numbers_text.split()]
                    if len(numbers) == 7:
                        red_numbers = numbers[:6]
                        blue_number = numbers[6]
                        lottery_data = {
                            'draw_number': issue,
                            'draw_date': draw_date,
                            'red_numbers': ','.join(red_numbers),
                            'blue_number': blue_number
                        }
                        lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    continue
            return lottery_list
        except Exception as e:
            return []

    def save_draws(self, draws: List[LotteryDraw]) -> None:
        try:
            self.ensure_data_dir()
            existing_issues = self.get_existing_issues()
            new_draws = [draw for draw in draws if draw.draw_number not in existing_issues]
            if not new_draws:
                return
            fieldnames = ['draw_number', 'draw_date', 'red_numbers', 'blue_number']
            rows = [draw.__dict__ for draw in new_draws]
            if not os.path.exists(self.data_file):
                with open(self.data_file, 'w', newline='') as f:
                    writer = csv.DictWriter(f, fieldnames=fieldnames)
                    writer.writeheader()
                    writer.writerows(rows)
            else:
                with open(self.data_file, 'a', newline='') as f:
                    writer = csv.DictWriter(f, fieldnames=fieldnames)
                    writer.writerows(rows)
        except Exception as e:
            try:
                with open(self.data_file, 'w', newline='') as f:
                    writer = csv.DictWriter(f, fieldnames=fieldnames)
                    writer.writeheader()
                    writer.writerows([draw.__dict__ for draw in draws])
            except Exception as e:
                raise

# KL8Crawler
class KL8Crawler(BaseCrawler):
    """快乐8爬虫"""
    def __init__(self):
        super().__init__('kl8')
        self.logger = logging.getLogger('kl8_crawler')
        self.config = LOTTERY_TYPES[self.lottery_type]
        self.headers = CRAWLER_CONFIG['headers']
        self.timeout = CRAWLER_CONFIG['timeout']
        self.data_file = self.config['data_file']
        self.ensure_data_dir()

    def ensure_data_dir(self):
        data_dir = os.path.dirname(self.data_file)
        if data_dir and not os.path.exists(data_dir):
            os.makedirs(data_dir)

    def get_existing_issues(self):
        if not os.path.exists(self.data_file):
            return set()
        with open(self.data_file, 'r') as f:
            reader = csv.DictReader(f)
            return {row['draw_number'] for row in reader}

    def validate_date(self, date_str: str) -> bool:
        try:
            date = datetime.strptime(date_str, '%Y-%m-%d')
            return date.date() <= datetime.now().date()
        except ValueError:
            return False

    def fetch_latest_draw(self) -> Optional[LotteryDraw]:
        try:
            response = requests.get(self.config['url'], headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            draw_divs = soup.find_all('div', class_='flex-auto w0')
            for div in draw_divs:
                try:
                    issue_tag = div.find('b', attrs={'data-name': 'issue'})
                    if not issue_tag:
                        continue
                    issue = issue_tag.get('data-v', '').replace('期', '')
                    time_tag = div.find('i', class_='mlauto')
                    if not time_tag:
                        continue
                    time_text = time_tag.text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        try:
                            draw_date = time_text.split()[0]
                            if not self.validate_date(draw_date):
                                self.logger.error(f"日期无效或为未来日期: {draw_date}")
                                continue
                        except (ValueError, IndexError):
                            self.logger.error(f"日期格式不正确: {time_text}")
                            continue
                    copy_link = div.find('a', attrs={'data-copy': 'true'})
                    if not copy_link:
                        continue
                    numbers_text = copy_link.get('data-v', '')
                    numbers = [num.strip() for num in numbers_text.split()]
                    if len(numbers) == 20:
                        lottery_data = {
                            'draw_number': issue,
                            'draw_date': draw_date,
                            'red_numbers': ','.join(numbers),
                            'blue_number': ''
                        }
                        lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    continue
            if lottery_list:
                return lottery_list[0]
            return None
        except Exception as e:
            return None

    def fetch_history_data(self) -> List[LotteryDraw]:
        try:
            response = requests.get(self.config['url'], headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            draw_divs = soup.find_all('div', class_='flex-auto w0')
            for div in draw_divs:
                try:
                    issue_tag = div.find('b', attrs={'data-name': 'issue'})
                    if not issue_tag:
                        continue
                    issue = issue_tag.get('data-v', '').replace('期', '')
                    time_tag = div.find('i', class_='mlauto')
                    if not time_tag:
                        continue
                    time_text = time_tag.text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        try:
                            draw_date = time_text.split()[0]
                            if not self.validate_date(draw_date):
                                self.logger.error(f"日期无效或为未来日期: {draw_date}")
                                continue
                        except (ValueError, IndexError):
                            self.logger.error(f"日期格式不正确: {time_text}")
                            continue
                    copy_link = div.find('a', attrs={'data-copy': 'true'})
                    if not copy_link:
                        continue
                    numbers_text = copy_link.get('data-v', '')
                    numbers = [num.strip() for num in numbers_text.split()]
                    if len(numbers) == 20:
                        lottery_data = {
                            'draw_number': issue,
                            'draw_date': draw_date,
                            'red_numbers': ','.join(numbers),
                            'blue_number': ''
                        }
                        lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    continue
            return lottery_list
        except Exception as e:
            return []

    def save_draws(self, draws: List[LotteryDraw]) -> None:
        try:
            self.ensure_data_dir()
            existing_issues = self.get_existing_issues()
            new_draws = [draw for draw in draws if draw.draw_number not in existing_issues]
            if not new_draws:
                return
            fieldnames = ['draw_number', 'draw_date', 'red_numbers', 'blue_number']
            rows = [draw.__dict__ for draw in new_draws]
            if not os.path.exists(self.data_file):
                with open(self.data_file, 'w', newline='') as f:
                    writer = csv.DictWriter(f, fieldnames=fieldnames)
                    writer.writeheader()
                    writer.writerows(rows)
            else:
                with open(self.data_file, 'a', newline='') as f:
                    writer = csv.DictWriter(f, fieldnames=fieldnames)
                    writer.writerows(rows)
        except Exception as e:
            try:
                with open(self.data_file, 'w', newline='') as f:
                    writer = csv.DictWriter(f, fieldnames=fieldnames)
                    writer.writeheader()
                    writer.writerows([draw.__dict__ for draw in draws])
            except Exception as e:
                raise

# QLCCrawler
class QLCCrawler(BaseCrawler):
    """七乐彩爬虫"""
    def __init__(self):
        super().__init__('7lc')
        self.logger = logging.getLogger('qlc_crawler')
        self.config = LOTTERY_TYPES[self.lottery_type]
        self.headers = CRAWLER_CONFIG['headers']
        self.timeout = CRAWLER_CONFIG['timeout']
        self.data_file = self.config['data_file']
        self.ensure_data_dir()

    def ensure_data_dir(self):
        data_dir = os.path.dirname(self.data_file)
        if data_dir and not os.path.exists(data_dir):
            os.makedirs(data_dir)

    def get_existing_issues(self):
        if not os.path.exists(self.data_file):
            return set()
        with open(self.data_file, 'r') as f:
            reader = csv.DictReader(f)
            return {row['draw_number'] for row in reader}

    def fetch_latest_draw(self):
        try:
            url = self.config['url']
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            draw_divs = soup.find_all('div', class_='flex p10')
            for div in draw_divs:
                try:
                    issue_tag = div.find('b', attrs={'data-name': 'issue'})
                    issue = issue_tag.get('data-v', '').replace('期', '')
                    time_tag = div.find('i', class_='mlauto')
                    if not time_tag:
                        continue
                    time_text = time_tag.text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        try:
                            draw_date = time_text.split()[0]
                        except (ValueError, IndexError):
                            continue
                    copy_link = div.find('a', attrs={'data-copy': 'true'})
                    if not copy_link:
                        continue
                    numbers_text = copy_link.get('data-v', '')
                    if '+' in numbers_text:
                        red_text, blue_text = numbers_text.split('+')
                        red_numbers = [num.strip() for num in red_text.split()]
                        blue_number = blue_text.strip()
                        if len(red_numbers) == 7:
                            lottery_data = {
                                'draw_number': issue,
                                'draw_date': draw_date,
                                'red_numbers': ','.join(red_numbers),
                                'blue_number': blue_number
                            }
                            lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    continue
            if lottery_list:
                return lottery_list[0]
            return None
        except Exception as e:
            return None

    def fetch_history_data(self):
        try:
            url = self.config['url']
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            lottery_list = []
            draw_divs = soup.find_all('div', class_='flex p10')
            for div in draw_divs:
                try:
                    issue_tag = div.find('b', attrs={'data-name': 'issue'})
                    issue = issue_tag.get('data-v', '').replace('期', '')
                    time_tag = div.find('i', class_='mlauto')
                    if not time_tag:
                        continue
                    time_text = time_tag.text.strip()
                    if '今天' in time_text:
                        draw_date = datetime.now().strftime('%Y-%m-%d')
                    elif '昨天' in time_text:
                        draw_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
                    else:
                        try:
                            draw_date = time_text.split()[0]
                        except (ValueError, IndexError):
                            continue
                    copy_link = div.find('a', attrs={'data-copy': 'true'})
                    if not copy_link:
                        continue
                    numbers_text = copy_link.get('data-v', '')
                    if '+' in numbers_text:
                        red_text, blue_text = numbers_text.split('+')
                        red_numbers = [num.strip() for num in red_text.split()]
                        blue_number = blue_text.strip()
                        if len(red_numbers) == 7:
                            lottery_data = {
                                'draw_number': issue,
                                'draw_date': draw_date,
                                'red_numbers': ','.join(red_numbers),
                                'blue_number': blue_number
                            }
                            lottery_list.append(LotteryDraw(**lottery_data))
                except Exception as e:
                    continue
            return lottery_list
        except Exception as e:
            return []

    def save_draws(self, draws: List[LotteryDraw]) -> None:
        try:
            self.ensure_data_dir()
            existing_issues = self.get_existing_issues()
            new_draws = [draw for draw in draws if draw.draw_number not in existing_issues]
            if not new_draws:
                return
            fieldnames = ['draw_number', 'draw_date', 'red_numbers', 'blue_number']
            rows = [draw.__dict__ for draw in new_draws]
            if not os.path.exists(self.data_file):
                with open(self.data_file, 'w', newline='') as f:
                    writer = csv.DictWriter(f, fieldnames=fieldnames)
                    writer.writeheader()
                    writer.writerows(rows)
            else:
                with open(self.data_file, 'a', newline='') as f:
                    writer = csv.DictWriter(f, fieldnames=fieldnames)
                    writer.writerows(rows)
        except Exception as e:
            try:
                with open(self.data_file, 'w', newline='') as f:
                    writer = csv.DictWriter(f, fieldnames=fieldnames)
                    writer.writeheader()
                    writer.writerows([draw.__dict__ for draw in draws])
            except Exception as e:
                raise 